﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace NamedBinaryTag.Converters
{
	class PathWalker
	{
		private static IDictionary<string, FixedTokenSource> tokenizedPaths;
		private AbstractTokenSource _source;
		private IList<Token> _tokenAccumulator;
		private string _path;

		static PathWalker()
		{
			tokenizedPaths = new Dictionary<string, FixedTokenSource>();
		}

		internal PathWalker(string path)
		{
			_path = path;
			if (tokenizedPaths.ContainsKey(path))
			{
				_source = tokenizedPaths[path];
				_tokenAccumulator = null;
			}
			else
			{
				_source = new PathTokenizer(path);
				_tokenAccumulator = new List<Token>();
			}
		}

		private Token ReadToken()
		{
			Token t = _source.ReadNext();
			if (_tokenAccumulator != null)
				_tokenAccumulator.Add(t);
			return t;
		}

		internal object Walk(ITag tag)
		{
			if (_tokenAccumulator == null)
				(_source as FixedTokenSource).Reset();
			object result = parsePath(tag);
			if (_tokenAccumulator != null)
			{
				FixedTokenSource newSource = new FixedTokenSource(_tokenAccumulator.ToArray());
				tokenizedPaths[_path] = newSource;
				_tokenAccumulator = null;
			}
			return result;
		}

		private object parsePath(ITag tag)
		{
			Token t = ReadToken();
			switch (t.Type)
			{
				case Token.TokenType.Text:
					if (isContextKeyword(tag, t.Value))
					{
						if ((_source.ReadAhead(1) != null) && (_source.ReadAhead(1).Type != Token.TokenType.EndOfString))
							throw new WalkException(string.Format("{0} Token at {1} is a Keyword in current context but it is not the last Token.", t.Type, t.Offset));
						return getContextKeywordValue(tag, t.Value);
					}
					tag = getNamedTag(tag, t.Value);
					break;
				case Token.TokenType.OpenBracket:
					object res = parseIndex(tag);
					if (res is ITag)
						tag = res as ITag;
					else
						return res;
					break;
				case Token.TokenType.EndOfString:
					return getTagValue(tag);
				default:
					throw new WalkException(string.Format("Unexpected {0} Token at {1}. Expected {2}, {3} or {4} Token.", t.Type, t.Offset, Token.TokenType.Text, Token.TokenType.OpenBracket, Token.TokenType.EndOfString));
			}
			if (_source.ReadAhead(0) == null)
				throw new NullReferenceException(string.Format("Unexpected null Token after {0} Token at {1}. Expected {2}, {3} or {4} Token.", t.Type, t.Offset, Token.TokenType.Dot, Token.TokenType.OpenBracket, Token.TokenType.EndOfString));
			if (_source.ReadAhead(0).Type == Token.TokenType.EndOfString)
			{
				ReadToken();
				return getTagValue(tag);
			}
			return parsePathNext(tag);
		}

		private object parsePathNext(ITag tag)
		{
			while (true)
			{
				Token t = ReadToken();
				switch (t.Type)
				{
					case Token.TokenType.Dot:
						t = ReadToken();
						if (t.Type != Token.TokenType.Text)
							throw new WalkException(string.Format("Unexpected {0} Token at {1}. Expected {2} Token.", t.Type, t.Offset, Token.TokenType.Text));
						if (isContextKeyword(tag, t.Value))
						{
							if ((_source.ReadAhead(1) != null) && (_source.ReadAhead(1).Type != Token.TokenType.EndOfString))
								throw new WalkException(string.Format("{0} Token at {1} is a Keyword in current context but it is not the last Token.", t.Type, t.Offset));
							return getContextKeywordValue(tag, t.Value);
						}
						tag = getNamedTag(tag, t.Value);
						break;
					case Token.TokenType.OpenBracket:
						object res = parseIndex(tag);
						if (res is ITag)
							tag = res as ITag;
						else
						{
							if (_source.ReadAhead(0) == null)
								throw new NullReferenceException(string.Format("Unexpected null Token after {0} Token at {1}. Expected {2}.", _source.LastToken.Type, _source.LastToken.Offset, Token.TokenType.EndOfString));
							if (_source.ReadAhead(0).Type == Token.TokenType.EndOfString)
							{
								ReadToken();
								return getTagValue(tag);
							}
							return res;
						}
						break;
					default:
						throw new WalkException(string.Format("Unexpected {0} Token at {1}. Expected {2} or {3} Token.", t.Type, t.Offset, Token.TokenType.Dot, Token.TokenType.OpenBracket));
				}
				if (_source.ReadAhead(0) == null)
					throw new NullReferenceException(string.Format("Unexpected null Token after {0} Token at {1}. Expected {2}, {3} or {4} Token.", t.Type, t.Offset, Token.TokenType.Dot, Token.TokenType.OpenBracket, Token.TokenType.EndOfString));
				if (_source.ReadAhead(0).Type == Token.TokenType.EndOfString)
				{
					ReadToken();
					return getTagValue(tag);
				}
			}
		}

		private object parseIndex(ITag tag)
		{
			Token t = ReadToken();
			object result = null;
			switch (t.Type)
			{
				case Token.TokenType.Integer:
					if (tag is TagByteArray)
						result = (tag as TagByteArray)[int.Parse(t.Value)];
					else if (tag is TagList)
						result = (tag as TagList).Items[int.Parse(t.Value)];
					else if (tag is TagCompound)
						result = (tag as TagCompound).Items[int.Parse(t.Value)];
					else
						throw new WalkException(string.Format("Unexpected {0} Tag for numeric index subelement retrieval. Expected {1}, {2} or {3}.", tag.GetType(), typeof(TagByteArray), typeof(TagList), typeof(TagCompound)));
					break;
				case Token.TokenType.Text:
					result = getNamedTag(tag, t.Value);
					break;
				default:
					throw new WalkException(string.Format("Unexpected {0} Token at {1}. Expected {2} or {3} Token.", t.Type, t.Offset, Token.TokenType.Integer, Token.TokenType.Text));
			}
			if (_source.ReadAhead(1) == null)
				throw new NullReferenceException(string.Format("Unexpected null Token after {0} Token at {1}. Expected {2} Token.", t.Type, t.Offset, Token.TokenType.CloseBracket));
			t = ReadToken();
			if (t.Type != Token.TokenType.CloseBracket)
				throw new WalkException(string.Format("Unexpected {0} Token at {1}. Expected {2}.", t.Type, t.Offset, Token.TokenType.CloseBracket));
			return result;
		}

		private ITag getNamedTag(ITag tag, string name)
		{
			if (!(tag is TagCompound))
				throw new WalkException(string.Format("Unexpected {0} Tag for named subtag retrieval. Expected {1}.", tag.GetType(), typeof(TagCompound)));
			IEnumerable<ITag> items = (tag as TagCompound).Items;
			foreach (ITag item in items)
			{
				if (item.Name == name)
					return item;
			}
			throw new WalkException(string.Format("Tag named {0} not found in compound.", name));
		}

		private object getTagValue(ITag tag)
		{
			if (tag is IValueTag)
				return tag.GetType().GetProperty("Value").GetValue(tag, null);
			if (tag is TagByteArray)
				return (tag as TagByteArray).Value;
			if (tag is TagList)
				return (tag as TagList).Items;
			if (tag is TagCompound)
				return (tag as TagCompound).Items;
			throw new WalkException(string.Format("Unexpected TagType {0}. Expected Byte, Short, Int, Long, Single, Double, Byte Array, List or Compound Tag.", tag.GetType()));
		}

		private bool isContextKeyword(ITag tag, string value)
		{
			if ((tag is TagList) || (tag is TagCompound))
			{
				if ((value == "Items") || (value == "Count"))
					return true;
			}
			if ((tag is TagList))
			{
				if ((value == "ListType"))
					return true;
			}
			if ((tag is IValueTag) || (tag is TagByteArray))
			{
				if ((value == "Value"))
					return true;
			}
			if ((value == "TagType") || (value == "Name"))
				return true;
			return false;
		}

		private object getContextKeywordValue(ITag tag, string value)
		{
			if ((tag is TagList) || (tag is TagCompound))
			{
				if (value == "Items")
					return tag.GetType().GetProperty("Items").GetValue(tag, null);
				if (value == "Count")
					return (tag.GetType().GetProperty("Items").GetValue(tag, null) as IList<ITag>).Count;
			}
			if ((tag is TagList))
			{
				if (value == "ListType")
					return (tag as TagList).ListType;
			}
			if ((tag is IValueTag) || (tag is TagByteArray))
			{
				if (value == "Value")
					return tag.GetType().GetProperty("Value").GetValue(tag, null);
			}
			if (value == "TagType")
				return tag.TagType;
			if (value == "Name")
				return tag.Name;
			throw new ArgumentException(string.Format("{0} is not a keyword for tag {1}.", value, tag.GetType()));
		}
	}
}
